Fix path traversal via metadata file_name in folder-based builders#8325
Fix path traversal via metadata file_name in folder-based builders#8325Kaif10 wants to merge 3 commits into
Conversation
…uggingface#8324) The `file_name` field from a dataset's metadata.jsonl/metadata.csv/metadata.parquet was normalized and joined to the metadata file's directory without any containment check. A crafted `file_name` such as "../../etc/passwd" or an absolute path escaped the dataset directory, letting a malicious dataset read arbitrary files on the host when loaded with imagefolder/audiofolder/videofolder/pdffolder (CWE-22). Reject any `file_name` that is absolute or uses ".." traversal to escape the directory containing the metadata file. The check is done on the relative reference so it works for both local directories and the fsspec URLs used for downloaded archives; for local paths it additionally resolves symlinks and verifies containment with os.path.commonpath. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lhoestq
left a comment
There was a problem hiding this comment.
Thanks for reporting and for the fix, I just have one comment:
| # For local paths, additionally resolve symlinks and confirm containment. | ||
| # Skipped for fsspec URLs (which contain "://") since realpath is not | ||
| # aware of them and would corrupt the URL. | ||
| if "://" not in item: |
There was a problem hiding this comment.
you can point to local files using file://../.. or local://../.. so this check is maybe not enough
maybe we need to forbid :// altogether ? (except for zip://<file_name>::<valid_relative_path>)
There was a problem hiding this comment.
You're right — file:// and local:// (and any other fsspec scheme) resolve to local files and slip past the containment check, since the old guard skipped it whenever file_name contained ://. On Linux the scheme also survives normpath, so this is a real bypass.
I've switched to forbidding :// in the file_name value entirely, as you suggested. Legitimate archive loads are unaffected: their URL is zip://<file_name>::<container>, and the scheme is always on the container (downloaded_metadata_dir), never on the file_name itself — so I now key the realpath containment check on the metadata directory rather than the joined path. Added regression tests for file:// / local:// payloads and confirmed the existing zip-archive tests (streaming and non-streaming) still pass.
(Disclosure: I used AI assistance while preparing this change.)
huggingface#8324) Addresses lhoestq's review on huggingface#8325: the `"://" not in item` guard let a crafted `file_name` such as `file://../..` or `local://../..` skip the realpath containment check, since these fsspec schemes resolve to local files. Per the maintainer's suggestion, forbid any URL scheme ("://") in the attacker-controlled `file_name`; legitimate archive reads are unaffected because their `zip://<file_name>::<container>` URL carries the scheme on the metadata directory (container), never on `file_name`. The containment check is now keyed on the metadata directory rather than the joined item. Adds regression tests for file:// and local:// scheme payloads and keeps the existing traversal/absolute/legit-subdir and zip-archive tests passing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lhoestq
left a comment
There was a problem hiding this comment.
Thanks ! I added a few more comments
| or file_relpath == ".." | ||
| or file_relpath.startswith("../") |
There was a problem hiding this comment.
if the path is like dummy/../../../ it can still escape
| if "://" not in downloaded_metadata_dir: | ||
| real_root = os.path.realpath(downloaded_metadata_dir) | ||
| real_path = os.path.realpath(item) |
There was a problem hiding this comment.
maybe you can have a check on both a chained url/path zip://...::... and a single url/path if you split downloaded_metadata_dir and item on :: to get only the first part before passing to commonpath() ?
we will need to make sure this works for these schemes:
/path/to/local/metadata_dirhf://path/to/remote/metadata_dirzip://metadata_dir::/path/to/localzip://metadata_dir::hf://path/to/remote
…uggingface#8324) Follow-up to lhoestq's second review on huggingface#8325. 1. `dummy/../../../` style references (a valid segment followed by an escape) are already normalized by `os.path.normpath` and rejected by the existing "../" guard; add regression tests to lock this in. 2. The containment check used to be skipped whenever the metadata directory was an fsspec URL. Make it run for every form by splitting both the metadata dir and the joined reference on the "::" hop separator and validating the first component (the part a relative `file_name` can influence), while requiring the trailing container component(s) to stay identical. URL components are compared with posixpath normalization (os.path mangles "//" and "\\" on Windows); genuine local paths still resolve symlinks via realpath + commonpath. Handles all four metadata-dir forms: local, hf:// remote, zip://...::<local>, and zip://...::hf://<remote>. Scheme rejection in `file_name` and the archive loading tests continue to pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks, both addressed. 1. 2. General containment check: done as you suggested — I split both
Added a test covering all four (legit relative passes, sibling-dir escape and container tampering rejected), and the existing scheme-rejection and zip-archive loading tests still pass. (Disclosure: I used AI assistance while preparing this change.) |
Fix #8324
Vulnerability
The folder-based builders (
imagefolder,audiofolder,videofolder,pdffolder) resolve each metadata row'sfile_nameby joining it to the metadata file's directory with no containment check. A malicious dataset can setfile_nameto a../-traversal path or an absolute path, causing the builder to open and read files outside the dataset directory when the dataset is loaded — arbitrary file read (CWE-22).Fix
FolderBasedBuilder._generate_examplesnow rejects, with aValueErrornaming the offendingfile_name, any value that is absolute or uses..traversal to escape the directory containing the metadata file. The validation is performed on the relative reference so it applies uniformly to local dataset directories and to the fsspec URLs used when reading from downloaded archives;os.path.realpath/os.path.commonpathwere deliberately not used on the joined path because those functions are not fsspec-aware (unlike the streaming-patchedos.path.join) and would corrupt archive URLs such aszip://...::.... For genuine local paths, symlinks are additionally resolved and containment is verified withos.path.commonpath. The containment root is the metadata file's own directory, matching the existing resolution base and preserving all legitimate relative references into subdirectories.Tests
Added regression tests in
tests/packaged_modules/test_folder_based_builder.py:../traversal rejected (three variants, includingsubdir/../../forms), absolute path rejected, and legitimate same-dir /subdir//./subdir/references still load. These fail on the pre-fix code and pass with the fix.test_folder_based_builder.py(51 passed) andtest_imagefolder.py(36 passed).Follows the containment approach of #8303.
AI assistance (Claude) was used in preparing this change; it was reviewed and verified locally.